package com.example.myproject.redis.distributionlock.springboot.case2;

import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.*;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class RedisLuaUtil2 {
    public final Logger logger = LoggerFactory.getLogger(RedisLuaUtil2.class);
    // Redis获取锁:setnx+设置过期时间的执行命令
    private final static String LUA_SCRIPT_LOCK = "return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) ";
    private static RedisScript<String> scriptLock = new DefaultRedisScript<String>(LUA_SCRIPT_LOCK, String.class);
    //Redis释放锁：通过value判定
    private static final String LUA_SCRIPT_UNLOCK =
            "if (redis.call('GET', KEYS[1]) == ARGV[1]) "
                    + "then return redis.call('DEL',KEYS[1]) "
                    + "else " + "return 0 " + "end";
    private static RedisScript<Long> scriptUnlock = new DefaultRedisScript<Long>(LUA_SCRIPT_UNLOCK, Long.class);

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Test
    public void concurrentTest() {
        int N = 4;
        ExecutorService executorService = Executors.newCachedThreadPool();
        CyclicBarrier barrier = new CyclicBarrier(N);
        for (int i = 0; i < N; i++) {
            executorService.execute(new doServiceRunnable(barrier));
        }
        executorService.shutdown();
        while (!executorService.isTerminated()) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("所有线程执行完毕...");
    }


    private class doServiceRunnable implements Runnable {

        private CyclicBarrier cyclicBarrier;

        public doServiceRunnable(CyclicBarrier cyclicBarrier) {
            this.cyclicBarrier = cyclicBarrier;
        }

        @Override
        public void run() {
            System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据...");
            try {
                Thread.sleep(5000);      //以睡眠来模拟写入数据操作
                cyclicBarrier.await();
                System.out.println("线程开始执行逻辑处理时间" + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                this.doLogic();
                System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕，等待其他线程写入完毕");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }  catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println("所有线程写入完毕，继续处理其他任务...");
        }

        public void doLogic() {
            boolean lock = false;
            RedisLock redisLock = null;
            String code = "code123456"; //  由一定的规则生成
            try {
                redisLock = lock(code);
                lock = (redisLock != null);
                if (lock) {//redis锁并发控制
                    String token = this.generateAccessToken("client_id_123", "client_secret_456");
                    //保存缓存的access_token到redis(授权码已从redis删除，生成的access_token可能因为网络中断或其他原因没传回去，故缓存到redis10分钟)
                    redisTemplate.opsForValue().set("TEST_TOKEN_" + code, token, 10, TimeUnit.MINUTES);
                    logger.info("返回新的token=" + token);
                }
            } catch (Exception e) {
                throw e;
            } finally {
                if (lock) {
                    //删除redis中的授权码
                    try {
//                    redisTemplate.delete("TEST_TOKEN_" + code);
                    } catch (Exception e) {
                        logger.error("删除redis授权码失败，code：" + code);
                    }
                    unlock(redisLock);
                }
            }
            if (!lock) {
//                throw new AppBusinessException(OauthErrorCode.REQUEST_QUICKLY_ERROR);REQUEST_QUICKLY_ERROR
            }
        }

        //获取锁
        private RedisLock lock(String code) {
            String flagKey = "TEST_REDIS_CONCURRENT_LOCK_" + code;//并发标志键值
            String uuid = UUID.randomUUID().toString();
            String expireTime = "1000";//过期时间/毫秒
            String execute = redisTemplate.execute(scriptLock, redisTemplate.getStringSerializer(), redisTemplate.getStringSerializer(),
                    Collections.singletonList(flagKey), uuid, expireTime);
            if (execute != null && execute.equals("OK")) {
                logger.info("线程" + Thread.currentThread().getName() + "===============>获取锁成功,授权码code：" + code);
                return new RedisLock(flagKey, uuid);
            }
            return null;
        }

        //释放锁
        private void unlock(RedisLock redisLock) {
            redisTemplate.execute(scriptUnlock, redisTemplate.getStringSerializer(), (RedisSerializer<Long>) redisTemplate.getKeySerializer(),
                    Collections.singletonList(redisLock.getKey()), redisLock.getUuid());
        }

        /**
         * 生成唯一AccessToken
         *
         * @param client_id
         * @param client_secret
         * @return
         */
        private String generateAccessToken(String client_id, String client_secret) {
            String md5Str = md5(client_id + client_secret + System.currentTimeMillis());
            return md5Str.substring(md5Str.length() - 12, md5Str.length()) + UUID.randomUUID().toString().replace("-", "");
        }

        /**
         * 通用md5加密方法
         *
         * @param text
         * @return
         */
        private String md5(String text) {
            if (text == null || StringUtils.isEmpty(text.trim()))
                return "";
            try {
                StringBuilder sb = new StringBuilder();
                MessageDigest md = MessageDigest.getInstance("MD5");
                md.update(text.getBytes(StandardCharsets.UTF_8));
                for (byte b : md.digest()) {
                    int n = b;
                    if (n < 0) n += 256;
                    if (n < 16) sb.append("0");
                    sb.append(Integer.toHexString(n));
                }
                return sb.toString();
            } catch (Exception e) {
                logger.error(e.getMessage(), e.getCause());
            }
            return null;
        }

    }
}
